package com.mapforge.map;

import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;

import com.mapforge.exceptions.LessThanOneDimensionException;

/** 
 * 
 */
public class TileMap {
	/** The tile layer's this map contains */
	private List<TileLayer> tileLayers;

	/** Scale this map renders at */
	private float scale;

	private int x;
	private int y;
	private int z;

	/** Width of this map in pixels */
	private int mapWidth;

	/** Height of this map in pixels */
	private int mapHeight;

	/** Tileset used by this map */
	private Tileset tileset;

	private int offsetX, offsetY;

	/**
	 * The name of this tile map
	 */
	private String name;

	/**
	 * The clip this map uses for rendering. Any tiles appearing entirely
	 * outside the clip will not be rendered.
	 */
	Rectangle clip;

	public TileMap(String name, int totalHorizontalTiles,
			int totalVerticalTiles, BufferedImage img, int tileWidth,
			int tileHeight) {
		this((Tileset) null, name, totalHorizontalTiles, totalVerticalTiles);
		Tileset tset = new Tileset(name + " tileset", img, tileWidth,
				tileHeight);
		this.tileset = tset;
		mapWidth = (int) (totalHorizontalTiles * tileset.getTileWidth() * scale);
		mapHeight = (int) (totalVerticalTiles * tileset.getTileHeight() * scale);
		offsetX = (int) (tileset.getTileWidth() * scale);
		offsetY = (int) (tileset.getTileHeight() * scale);
		x = (int) (x * scale);
		y = (int) (y * scale);
	}

	/**
	 * 
	 * @param tileset
	 *            represents all possible tiles in this map
	 * @param totalHorizontalTiles
	 *            total number of horizontal tiles in this tiled map
	 * @param totalVerticalTiles
	 *            total number of vertical tiles in this tiled map
	 * @throws LessThanOneDimensionException
	 *             if total horizontal or vertical tiles isn't greater than 0
	 */
	public TileMap(Tileset tileset, String name, int totalHorizontalTiles,
			int totalVerticalTiles) throws LessThanOneDimensionException,
			IllegalArgumentException {
		if (totalHorizontalTiles < 1)
			throw new LessThanOneDimensionException(
					"Tiled map width must be greater than 0.");
		if (totalVerticalTiles < 1)
			throw new LessThanOneDimensionException(
					"Tild map height must be greater than 0.");
		x = y = 0;
		this.name = name;
		this.tileset = tileset;
		scale = 1.0f;
		if (tileset != null) {
			mapWidth = (int) (totalHorizontalTiles * tileset.getTileWidth() * scale);
			mapHeight = (int) (totalVerticalTiles * tileset.getTileHeight() * scale);
			x = (int) (x * scale);
			y = (int) (y * scale);
			offsetX = (int) (tileset.getTileWidth() * scale);
			offsetY = (int) (tileset.getTileHeight() * scale);
		}
		tileLayers = new ArrayList<TileLayer>();
		clip = new Rectangle();
	}

	public int getXOffset() {
		return offsetX;
	}

	public int getYOffset() {
		return offsetY;
	}

	public int getClipX() {
		return clip.x - offsetX;
	}

	public int getClipWidth() {
		return clip.width + offsetX;
	}

	public int getClipHeight() {
		return clip.height + offsetY;
	}

	public int getClipY() {
		return clip.y - offsetY;
	}

	/**
	 * Sets this map's name.
	 * 
	 * @param name
	 *            map's new name
	 */
	public void setName(String name) {
		this.name = name;
	}

	/**
	 * Returns this map's name.
	 * 
	 * @return this map's name
	 */
	public String getName() {
		return this.name;
	}

	/**
	 * Returns the width of this map in pixels
	 * 
	 * @return width of map in pixels
	 */
	public int getWidth() {
		return this.mapWidth;
	}

	/**
	 * Returns the height of this map in pixels.
	 * 
	 * @return height of map in pixels
	 */
	public int getHeight() {
		return this.mapHeight;
	}

	/**
	 * Returns the scale at which tiles are rendered by this map.
	 * 
	 * @return scale tiles are rendered at
	 */
	public float getScale() {
		return this.scale;
	}

	public void setClip(int x, int y, int width, int height) {
		setClipBounds(width, height);
		setClipLocation(x, y);
	}

	public void setClipLocation(int x, int y) {
		clip.x = Math.min(getWidth() - clip.width, Math.max(0, x));
		clip.y = Math.min(getHeight() - clip.height, Math.max(0, y));
	}

	public void setClipBounds(int w, int h) {
		clip.width = Math.min(w, getWidth());
		clip.height = Math.min(h, getHeight());
	}

	/**
	 * This sets the scale at which tiles are rendered by this map.
	 * 
	 * @param scale
	 *            specified scale with which to render tiles
	 */
	public void setScale(float scale) {
		if (scale <= 0)
			throw new IllegalArgumentException(
					"Tiled map scale must be greater than 0.");
		int checkTW = (int) (tileset.getTileWidth() * scale);
		int checkTH = (int) (tileset.getTileHeight() * scale);
		if (checkTW == 0 || checkTH == 0) {
			throw new IllegalArgumentException(
					"Tile map tiles wouldn't appear at " + scale + " scale");
		}

		this.mapWidth = (int) (this.getTotalHorizontalTiles()
				* tileset.getTileWidth() * scale);
		this.mapHeight = (int) (this.getTotalVerticalTiles()
				* tileset.getTileHeight() * scale);
		this.offsetX = (int) (tileset.getTileWidth() * scale);
		this.offsetY = (int) (tileset.getTileHeight() * scale);

		this.scale = scale;
	}

	/**
	 * Sorts all {@link TileLayer tile layers} this {@link TileMap} contains by
	 * their z value.
	 * 
	 * @see TileLayer#getZ()
	 * @see #sortLayers(Comparator)
	 */
	public void sortLayers() {
		Collections.sort(tileLayers);
	}

	/**
	 * Sorts all {@link TileLayer tile layers} this {@link TileMap} contains
	 * with the specified {@link Comparator}.
	 * 
	 * @see #sortLayers()
	 * @param comp
	 *            specified comparator
	 */
	public void sortLayers(Comparator<TileLayer> comp) {
		Collections.sort(tileLayers, comp);
	}

	/**
	 * Adds the specified {@link TileLayer} to this {@link TileMap TileMap.}
	 * 
	 * @see #removeTileLayer(TileLayer)
	 * @param layer
	 *            specified {@link TileLayer}
	 */
	public void addTileLayer(TileLayer layer) {
		this.tileLayers.add(layer);
	}

	/**
	 * Removes all {@link TileLayer tile layers} this {@link TileMap} contains.
	 * 
	 * @see #addTileLayer(TileLayer)
	 * @see #removeTileLayer(TileLayer)
	 */
	public void clearTileLayers() {
		tileLayers.clear();
	}

	/**
	 * Removes the specified {@link TileLayer} from this {@link TileMap}, and
	 * returns whether this was successful.
	 * <p>
	 * Note this method will return false if the specified {@link TileLayer}
	 * isn't contained within this {@link TileMap TileMap.}
	 * </p>
	 * 
	 * @see #addTileLayer(TileLayer)
	 * @param layer
	 *            specified {@link TileLayer} to remove
	 * @return whether the specified {@link TileLayer} was removed successfully.
	 */
	public boolean removeTileLayer(TileLayer layer) {
		return this.tileLayers.remove(layer);
	}

	/**
	 * Returns the number of {@link TileLayer tile layers} this map contains.
	 * 
	 * @return how many {@link TileLayer tile layers} this map contains
	 */
	public int getNumberTileLayers() {
		return tileLayers.size();
	}

	public Iterator<TileLayer> getTileLayerIterator() {
		return tileLayers.iterator();
	}

	/**
	 * Returns the width of {@link Tile tiles} in this {@link TileMap}.
	 * 
	 * @return width of {@link Tile tiles} in this {@link TileMap}
	 */
	public int getTileWidth() {
		return (int) (tileset.getTileWidth() * scale);
	}

	/**
	 * Returns the height of {@link Tile tiles} in this {@link TileMap}.
	 * 
	 * @return height of {@link Tile tiles} in this {@link TileMap}
	 */
	public int getTileHeight() {
		return (int) (tileset.getTileHeight() * scale);
	}

	/** */
	public void setTileset(Tileset tileset) {
		if (this.tileset != null) {
			int totalHorizontalTiles = mapWidth / this.tileset.getTileWidth();
			int totalVerticalTiles = mapHeight / this.tileset.getTileHeight();
			mapWidth = (int) (totalHorizontalTiles * tileset.getTileWidth() * scale);
			mapHeight = (int) (totalVerticalTiles * tileset.getTileHeight() * scale);
			offsetX = (int) (tileset.getTileWidth() * scale);
			offsetY = (int) (tileset.getTileHeight() * scale);
		}
		this.tileset = tileset;
	}

	/** */
	public Tileset getTileset() {
		return tileset;
	}

	public Rectangle calculateTileClip() {
		int tileWidth = getTileWidth();
		int tileHeight = getTileHeight();
		int modOffset = clip.x % tileWidth;
		int clipX = (int) ((clip.x - offsetX - modOffset) / scale);
		modOffset = clip.y % getTileHeight();
		int clipY = (int) ((clip.y - offsetY - modOffset) / scale);

		// starting and ending tile x locations
		int startTileX = (int) Math.max(clipX / tileset.getTileWidth(), getX()
				/ tileWidth);
		int width = (int) Math.min(1 / scale * clip.width
				/ tileset.getTileWidth() + offsetX * 2 / tileWidth,
				getTotalHorizontalTiles());

		// starting and ending tile y location
		int startTileY = (int) Math.max(clipY / tileset.getTileHeight(), getY()
				/ tileHeight);
		int height = (int) Math.min(1 / scale * clip.height
				/ tileset.getTileHeight() + offsetY * 2 / tileHeight,
				getTotalVerticalTiles());
		return new Rectangle(startTileX, startTileY, width, height);
	}

	/** */
	public void render(Graphics2D g) {
		int tileWidth = getTileWidth();
		int tileHeight = getTileHeight();
		int modOffset = clip.x % tileWidth;
		int clipX = (int) ((clip.x - offsetX - modOffset) / scale);
		modOffset = clip.y % getTileHeight();
		int clipY = (int) ((clip.y - offsetY - modOffset) / scale);

		// unscaled starting x render location
		int startx = (int) Math.max(clipX, 0);

		// starting and ending tile x locations
		int startTileX = (int) Math.max(clipX / tileset.getTileWidth(), getX()
				/ tileWidth);
		int endTileX = (int) Math.min(startTileX + 1 / scale * clip.width
				/ tileset.getTileWidth() + offsetX * 2 / tileWidth,
				getTotalHorizontalTiles());

		// unscaled starting y render location
		int starty = (int) Math.max(clipY, 0);

		// starting and ending tile y location
		int startTileY = (int) Math.max(clipY / tileset.getTileHeight(), getY()
				/ tileHeight);
		int endTileY = (int) Math.min(startTileY + 1 / scale * clip.height
				/ tileset.getTileHeight() + offsetY * 2 / tileHeight,
				getTotalVerticalTiles());

		g.scale(scale, scale);
		// iterate over valid clip for all layers iterate
		for (int i = 0; i < tileLayers.size(); i++) {
			tileLayers.get(i).render(g, startx, starty, startTileX, startTileY,
					endTileX, endTileY);
		}
		g.scale(1, 1);
	}

	public void setX(int x) {
		this.x = x;
	}

	public void setY(int y) {
		this.y = y;
	}

	public void setZ(int z) {
		this.z = z;
	}

	public int getZ() {
		return z;
	}

	public int getX() {
		return x;
	}

	public int getY() {
		return this.y;
	}

	/**
	 * Returns the total number of horizontal tiles this map contains.
	 * 
	 * @return total number of horizontal tiles
	 */
	public int getTotalHorizontalTiles() {
		return (int) (this.mapWidth / this.tileset.getTileWidth() / this.scale);
	}

	public void setTotalHorizontalTiles(int horizontalTiles) {
		this.mapWidth = (int) (horizontalTiles * (this.tileset.getTileWidth() / this.scale));
	}

	/**
	 * Returns the total number of vertical tiles this map contains.
	 * 
	 * @return total number of vertical tiles
	 */
	public int getTotalVerticalTiles() {
		return (int) (this.mapHeight / this.tileset.getTileHeight() / this.scale);
	}

	public void setTotalVerticalTiles(int verticalTiles) {
		this.mapHeight = (int) (verticalTiles * (this.tileset.getTileHeight() / this.scale));
	}
}
